package typeexpr

import (
    "fmt"

    "Havoc/pkg/profile/yaotl"
    "github.com/zclconf/go-cty/cty"
)

const invalidTypeSummary = "Invalid type specification"

// getType is the internal implementation of both Type and TypeConstraint,
// using the passed flag to distinguish. When constraint is false, the "any"
// keyword will produce an error.
func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
    // First we'll try for one of our keywords
    kw := hcl.ExprAsKeyword(expr)
    switch kw {
    case "bool":
        return cty.Bool, nil
    case "string":
        return cty.String, nil
    case "number":
        return cty.Number, nil
    case "any":
        if constraint {
            return cty.DynamicPseudoType, nil
        }
        return cty.DynamicPseudoType, hcl.Diagnostics{{
            Severity: hcl.DiagError,
            Summary:  invalidTypeSummary,
            Detail:   fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw),
            Subject:  expr.Range().Ptr(),
        }}
    case "list", "map", "set":
        return cty.DynamicPseudoType, hcl.Diagnostics{{
            Severity: hcl.DiagError,
            Summary:  invalidTypeSummary,
            Detail:   fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw),
            Subject:  expr.Range().Ptr(),
        }}
    case "object":
        return cty.DynamicPseudoType, hcl.Diagnostics{{
            Severity: hcl.DiagError,
            Summary:  invalidTypeSummary,
            Detail:   "The object type constructor requires one argument specifying the attribute types and values as a map.",
            Subject:  expr.Range().Ptr(),
        }}
    case "tuple":
        return cty.DynamicPseudoType, hcl.Diagnostics{{
            Severity: hcl.DiagError,
            Summary:  invalidTypeSummary,
            Detail:   "The tuple type constructor requires one argument specifying the element types as a list.",
            Subject:  expr.Range().Ptr(),
        }}
    case "":
        // okay! we'll fall through and try processing as a call, then.
    default:
        return cty.DynamicPseudoType, hcl.Diagnostics{{
            Severity: hcl.DiagError,
            Summary:  invalidTypeSummary,
            Detail:   fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
            Subject:  expr.Range().Ptr(),
        }}
    }

    // If we get down here then our expression isn't just a keyword, so we'll
    // try to process it as a call instead.
    call, diags := hcl.ExprCall(expr)
    if diags.HasErrors() {
        return cty.DynamicPseudoType, hcl.Diagnostics{{
            Severity: hcl.DiagError,
            Summary:  invalidTypeSummary,
            Detail:   "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).",
            Subject:  expr.Range().Ptr(),
        }}
    }

    switch call.Name {
    case "bool", "string", "number", "any":
        return cty.DynamicPseudoType, hcl.Diagnostics{{
            Severity: hcl.DiagError,
            Summary:  invalidTypeSummary,
            Detail:   fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
            Subject:  &call.ArgsRange,
        }}
    }

    if len(call.Arguments) != 1 {
        contextRange := call.ArgsRange
        subjectRange := call.ArgsRange
        if len(call.Arguments) > 1 {
            // If we have too many arguments (as opposed to too _few_) then
            // we'll highlight the extraneous arguments as the diagnostic
            // subject.
            subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range())
        }

        switch call.Name {
        case "list", "set", "map":
            return cty.DynamicPseudoType, hcl.Diagnostics{{
                Severity: hcl.DiagError,
                Summary:  invalidTypeSummary,
                Detail:   fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name),
                Subject:  &subjectRange,
                Context:  &contextRange,
            }}
        case "object":
            return cty.DynamicPseudoType, hcl.Diagnostics{{
                Severity: hcl.DiagError,
                Summary:  invalidTypeSummary,
                Detail:   "The object type constructor requires one argument specifying the attribute types and values as a map.",
                Subject:  &subjectRange,
                Context:  &contextRange,
            }}
        case "tuple":
            return cty.DynamicPseudoType, hcl.Diagnostics{{
                Severity: hcl.DiagError,
                Summary:  invalidTypeSummary,
                Detail:   "The tuple type constructor requires one argument specifying the element types as a list.",
                Subject:  &subjectRange,
                Context:  &contextRange,
            }}
        }
    }

    switch call.Name {

    case "list":
        ety, diags := getType(call.Arguments[0], constraint)
        return cty.List(ety), diags
    case "set":
        ety, diags := getType(call.Arguments[0], constraint)
        return cty.Set(ety), diags
    case "map":
        ety, diags := getType(call.Arguments[0], constraint)
        return cty.Map(ety), diags
    case "object":
        attrDefs, diags := hcl.ExprMap(call.Arguments[0])
        if diags.HasErrors() {
            return cty.DynamicPseudoType, hcl.Diagnostics{{
                Severity: hcl.DiagError,
                Summary:  invalidTypeSummary,
                Detail:   "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.",
                Subject:  call.Arguments[0].Range().Ptr(),
                Context:  expr.Range().Ptr(),
            }}
        }

        atys := make(map[string]cty.Type)
        for _, attrDef := range attrDefs {
            attrName := hcl.ExprAsKeyword(attrDef.Key)
            if attrName == "" {
                diags = append(diags, &hcl.Diagnostic{
                    Severity: hcl.DiagError,
                    Summary:  invalidTypeSummary,
                    Detail:   "Object constructor map keys must be attribute names.",
                    Subject:  attrDef.Key.Range().Ptr(),
                    Context:  expr.Range().Ptr(),
                })
                continue
            }
            aty, attrDiags := getType(attrDef.Value, constraint)
            diags = append(diags, attrDiags...)
            atys[attrName] = aty
        }
        return cty.Object(atys), diags
    case "tuple":
        elemDefs, diags := hcl.ExprList(call.Arguments[0])
        if diags.HasErrors() {
            return cty.DynamicPseudoType, hcl.Diagnostics{{
                Severity: hcl.DiagError,
                Summary:  invalidTypeSummary,
                Detail:   "Tuple type constructor requires a list of element types.",
                Subject:  call.Arguments[0].Range().Ptr(),
                Context:  expr.Range().Ptr(),
            }}
        }
        etys := make([]cty.Type, len(elemDefs))
        for i, defExpr := range elemDefs {
            ety, elemDiags := getType(defExpr, constraint)
            diags = append(diags, elemDiags...)
            etys[i] = ety
        }
        return cty.Tuple(etys), diags
    default:
        // Can't access call.Arguments in this path because we've not validated
        // that it contains exactly one expression here.
        return cty.DynamicPseudoType, hcl.Diagnostics{{
            Severity: hcl.DiagError,
            Summary:  invalidTypeSummary,
            Detail:   fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
            Subject:  expr.Range().Ptr(),
        }}
    }
}
